
package com.bangcommunity.bbframe.springredis;

import com.bangcommunity.bbframe.common.base.MainConfigLoader;
import com.bangcommunity.bbframe.common.tuple.MutablePair;
import com.bangcommunity.bbframe.common.tuple.Pair;
import com.bangcommunity.bbframe.common.utils.io.PropertiesUtil;
import com.bangcommunity.bbframe.common.utils.lang.CollectionUtils;
import com.bangcommunity.bbframe.common.utils.lang.StringUtils;
import com.bangcommunity.bbframe.dynconfig.DynConfigRegister;
import com.bangcommunity.bbframe.dynconfig.IDynConfigProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.util.JedisURIHelper;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Created by tanghc on 17/1/13.
 *
 * 获得项目相关的redis客户端。 建议在使用cache的地方调用 QtProjectRedisClient.getSpringRedisCache()获取
 * 这样可以保持最新的客户端(若redis配置信息修改了,会重新生成客户端) 否则始终保持一个客户端必须修改redis必须重启
 *
 */
public class ProjectRedisClient {

    /** 读写锁保证对象更新时线程安全 */
    public static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final static Logger logger = LoggerFactory.getLogger(ProjectRedisClient.class);

    private ProjectRedisClient() {
    }

    private static class ProjectRedisClientHolder {

        public static ISpringRedisCache instance = null;
        private static final String redis_config_group = "common";
        private static final String redis_config_dataId = "redisConfig";
        private static final String productCode = MainConfigLoader.getInstance().getProductCode();
        private static final String appCode = MainConfigLoader.getInstance().getAppCode();
        private static JedisConnectionFactory oldJedisConnectionFactory = null;
        static {
            DynConfigRegister.getInstance().buildOnce(productCode, appCode, redis_config_group, redis_config_dataId,
                    new IDynConfigProcessor() {

                        @Override
                        public void procesConfig(String config) {
                            if (null != oldJedisConnectionFactory) {
                                logger.info("destroy oldJedisConnectionFactory when modify redis config");
                                oldJedisConnectionFactory.destroy();
                            }
                            Properties properties = PropertiesUtil.loadFromString(config);
                            JedisConnectionFactory jedisConnectionFactory = buildNewJedisConnectionFactory(properties);
                            if (null != jedisConnectionFactory) {
                                ISpringRedisCache springRedisCache = buildNewRedisRepository(jedisConnectionFactory,
                                        properties);
                                // 创建客户端成功情况下才修改单例
                                if (null != springRedisCache) {

                                    // Lock lock = ProjectRedisClient.readWriteLock.writeLock();
                                    try {
                                        // lock.lock();
                                        oldJedisConnectionFactory = jedisConnectionFactory;
                                        instance = springRedisCache;
                                        logger.warn(
                                                "modify redis info create new redisclient:productCode={},appCode={}",
                                                productCode, appCode);
                                    } finally {
                                        // lock.unlock();
                                    }
                                } else {
                                    logger.warn(
                                            "didnot modify redis info create new redisclient no springrediscache:productCode={},appCode={}",
                                            productCode, appCode);
                                }
                            } else {
                                logger.warn(
                                        "didnot modify redis info create new redisclient no jedisconnection factory :productCode={},appCode={}",
                                        productCode, appCode);
                            }

                        }
                    });
        }

        private static String getProjectPrefix(String productCode, String appCode) {

            return new StringBuilder().append(productCode).append(":").append(appCode).toString();
        }

        /**
         * always return new
         *
         * @param properties
         * @return
         */
        public static JedisConnectionFactory buildNewJedisConnectionFactory(Properties properties) {

            String aTrue = properties.getProperty("redis.usePool", "true");
            boolean usePool = Boolean.valueOf(aTrue);

            JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();

            JedisShardInfo jedisShardInfo = buildJedisShardInfo(properties);
            RedisSentinelConfiguration redisSentinelConfiguration = buildRedisSentinelConfiguration(properties);
            RedisClusterConfiguration redisClusterConfiguration = buildRedisClusterConfiguration(properties);
            if (null != redisSentinelConfiguration) {
                jedisConnectionFactory = new JedisConnectionFactory(redisSentinelConfiguration);
                jedisConnectionFactory.setShardInfo(jedisShardInfo);
            }
            if (null != redisClusterConfiguration) {
                jedisConnectionFactory = new JedisConnectionFactory(redisClusterConfiguration);
            }
            if (null != jedisShardInfo) {
                jedisConnectionFactory.setShardInfo(jedisShardInfo);
            }
            if (usePool) {
                JedisPoolConfig jedisPoolConfig = buildJedisPoolConfig(properties);
                jedisConnectionFactory.setPoolConfig(jedisPoolConfig);
            }
            if (null != jedisConnectionFactory) {
                String passwd = properties.getProperty("redis.password", "");
                String timeout = properties.getProperty("redis.timeout", "2000");
                String dbIndex = properties.getProperty("redis.db", "0");
                if (StringUtils.isNotBlank(passwd)) {
                    jedisConnectionFactory.setPassword(passwd);
                }
                if (StringUtils.isNotBlank(timeout)) {
                    jedisConnectionFactory.setTimeout(Integer.parseInt(timeout));
                }
                if (StringUtils.isNotBlank(dbIndex)) {
                    jedisConnectionFactory.setDatabase(Integer.parseInt(dbIndex));
                }
                // important
                jedisConnectionFactory.afterPropertiesSet();
            }
            logger.info("init redisclient : port={},host={},passwd={},db={},share={}",jedisConnectionFactory.getPort(),jedisConnectionFactory.getHostName(),jedisConnectionFactory.getPassword(),jedisConnectionFactory.getDatabase(),jedisConnectionFactory.getShardInfo());
            return jedisConnectionFactory;
        }

        private static JedisShardInfo buildJedisShardInfo(Properties properties) {
            String url = properties.getProperty("redis.url", "");
            JedisShardInfo jedisShardInfo = null;
            String passwd = properties.getProperty("redis.password", "");
            if (StringUtils.isBlank(url)) {
                String host = properties.getProperty("redis.host", "127.0.0.1");
                if (StringUtils.isNotBlank(host)) {
                    String portStr = properties.getProperty("redis.port", "6379");
                    String dbStr = properties.getProperty("redis.db", "0");
                    StringBuilder stringBuilder = new StringBuilder();
                    stringBuilder.append("http://");
                    if (StringUtils.isNotBlank(passwd)) {
                        stringBuilder.append("password:").append(passwd).append("@");
                    }
                    if (StringUtils.isBlank(dbStr)) {
                        dbStr = "0";
                    }
                    stringBuilder.append(host).append(":").append(portStr).append("/").append(dbStr);
                    url = stringBuilder.toString();
                }
            }
            if (StringUtils.isNotBlank(url)) {
                jedisShardInfo = new JedisShardInfo(url);
            }

            logger.warn("redis  set shard info: {}", url);
            return jedisShardInfo;
        }

        private static RedisClusterConfiguration buildRedisClusterConfiguration(Properties properties) {
            String cluster = properties.getProperty("redis.cluster.nodes", "");
            cluster = StringUtils.trimAllWhitespace(cluster);
            if (StringUtils.isNotBlank(cluster)) {

                int maxRedirects = Integer.parseInt(properties.getProperty("redis.cluster.maxRedirects"));
                Set<String> hostAndPortsSet = getHostAndPortsSet(cluster);
                if (CollectionUtils.isNotEmpty(hostAndPortsSet)) {
                    RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(hostAndPortsSet);
                    clusterConfiguration.setMaxRedirects(maxRedirects);
                    logger.warn("redis use cluster factory and set info: {}", cluster);
                    return clusterConfiguration;
                }
            }
            return null;
        }

        private static RedisSentinelConfiguration buildRedisSentinelConfiguration(Properties properties) {
            String sentinel = properties.getProperty("redis.sentinel", "");
            sentinel = StringUtils.trimAllWhitespace(sentinel);
            if (StringUtils.isNotBlank(sentinel)) {
                Set<String> sentinelHostAndPorts = getHostAndPortsSet(sentinel);
                if (CollectionUtils.isNotEmpty(sentinelHostAndPorts)) {
                    RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration(
                            properties.getProperty("redis.sentinel.masterName", "mymaster"), sentinelHostAndPorts);
                    logger.warn("redis use sentinel factory and set info: {}", sentinel);
                    return sentinelConfig;
                }
            }
            return null;
        }

        /**
         * 解析地址中host:port集合
         * 
         * @param rawUrl
         * @return
         */
        private static Set<String> getHostAndPortsSet(String rawUrl) {
            rawUrl = StringUtils.trimAllWhitespace(rawUrl);
            if (StringUtils.isNotBlank(rawUrl)) {
                List<Pair<String, Integer>> hostPorts = parseHostPorts(rawUrl);
                Set<String> sentinelHostAndPorts = new HashSet<>();
                if (null != hostPorts) {
                    for (Pair<String, Integer> pair : hostPorts) {
                        sentinelHostAndPorts.add(pair.getLeft() + ":" + pair.getRight());
                    }
                }
                return sentinelHostAndPorts;
            }
            return null;
        }

        private static JedisPoolConfig buildJedisPoolConfig(Properties properties) {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxIdle(Integer.parseInt(
                    properties.getProperty("redis.pool.maxIdle", String.valueOf(jedisPoolConfig.getMaxIdle()))));
            jedisPoolConfig.setMaxTotal(Integer.parseInt(
                    properties.getProperty("redis.pool.maxTotal", String.valueOf(jedisPoolConfig.getMaxTotal()))));
            jedisPoolConfig.setMaxWaitMillis(Integer.parseInt(properties.getProperty("redis.pool.maxWaitMillis",
                    String.valueOf(jedisPoolConfig.getMaxWaitMillis()))));
            jedisPoolConfig.setMinEvictableIdleTimeMillis(
                    Integer.parseInt(properties.getProperty("redis.pool.minEvictableIdleTimeMillis",
                            String.valueOf(jedisPoolConfig.getMinEvictableIdleTimeMillis()))));
            jedisPoolConfig.setMinIdle(Integer.parseInt(
                    properties.getProperty("redis.pool.minIdle", String.valueOf(jedisPoolConfig.getMinIdle()))));
            jedisPoolConfig.setNumTestsPerEvictionRun(Integer.parseInt(properties.getProperty(
                    "redis.pool.numTestsPerEvictionRun", String.valueOf(jedisPoolConfig.getNumTestsPerEvictionRun()))));
            jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(
                    Integer.parseInt(properties.getProperty("redis.pool.minEvictableIdleTimeMillis",
                            String.valueOf(jedisPoolConfig.getMinEvictableIdleTimeMillis()))));
            jedisPoolConfig.setTestOnBorrow(Boolean.parseBoolean(properties.getProperty("redis.pool.testOnBorrow",
                    String.valueOf(jedisPoolConfig.getTestOnBorrow()))));
            jedisPoolConfig.setTestOnReturn(Boolean.parseBoolean(properties.getProperty("redis.pool.testOnReturn",
                    String.valueOf(jedisPoolConfig.getTestOnReturn()))));
            jedisPoolConfig.setTestWhileIdle(Boolean.parseBoolean(properties.getProperty("redis.pool.testWhileIdle",
                    String.valueOf(jedisPoolConfig.getTestWhileIdle()))));
            jedisPoolConfig.setTimeBetweenEvictionRunsMillis(
                    Integer.parseInt(properties.getProperty("redis.pool.timeBetweenEvictionRunsMillis",
                            String.valueOf(jedisPoolConfig.getTimeBetweenEvictionRunsMillis()))));
            return jedisPoolConfig;
        }

        /**
         * always return new
         *
         * @param properties
         * @return
         */
        public static ISpringRedisCache buildNewRedisRepository(Properties properties) {

            RedisTemplate redisTemplate = new RedisTemplate();
            redisTemplate.setConnectionFactory(buildNewJedisConnectionFactory(properties));

            // StringRedisSerializer
            StringRedisSerializer serializer = new StringRedisSerializer();
            redisTemplate.setKeySerializer(serializer);
            redisTemplate.setHashKeySerializer(serializer);
            // redisTemplate.setHashValueSerializer(serializer);
            redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());

            // important
            redisTemplate.afterPropertiesSet();

            RedisRepository redisRepository = new RedisRepository();
            redisRepository.setRedisTemplate(redisTemplate);
            redisRepository.setPrefix(getProjectPrefix(productCode, appCode));

            return redisRepository;
        }

        /**
         * always return new
         *
         * @param properties
         * @return
         */
        public static ISpringRedisCache buildNewRedisRepository(JedisConnectionFactory jedisConnectionFactory,
                Properties properties) {

            RedisTemplate redisTemplate = new RedisTemplate();
            redisTemplate.setConnectionFactory(jedisConnectionFactory);

            // StringRedisSerializer
            StringRedisSerializer serializer = new StringRedisSerializer();
            redisTemplate.setKeySerializer(serializer);
            redisTemplate.setHashKeySerializer(serializer);
//             redisTemplate.setHashValueSerializer(serializer);
//            redisTemplate.setDefaultSerializer(serializer);

            // important
            redisTemplate.afterPropertiesSet();

            RedisRepository redisRepository = new RedisRepository();
            redisRepository.setRedisTemplate(redisTemplate);
            String aTrue = properties.getProperty("redis.useNameSpace", "true");
            boolean useNameSpace = Boolean.valueOf(aTrue);
            if (useNameSpace) {
                redisRepository.setPrefix(getProjectPrefix(productCode, appCode));
            }

            return redisRepository;
        }

        /**
         * 原地址可以是 protol://host:port或者是host:port。多个使用","或者";"隔开 解析地址成 host:port 格式
         * 
         * @param rawUrl
         * @return
         */
        private static List<Pair<String, Integer>> parseHostPorts(String rawUrl) {
            String[] split = null;
            if (rawUrl.indexOf(",") != -1) {
                split = rawUrl.split(",");
            } else {
                split = rawUrl.split(";");
            }
            List<Pair<String, Integer>> hostPorts = new ArrayList<>();
            if (null != split && split.length > 0) {
                for (String s : split) {
                    if (s.indexOf("://") == -1) {
                        s = "http://" + s;
                    }
                    try {
                        URI uri = URI.create(s);
                        if (JedisURIHelper.isValid(uri)) {
                            hostPorts.add(new MutablePair<>(uri.getHost(), uri.getPort()));
                        }
                    } catch (Exception e) {
                        logger.warn("parse redis sentinel config invalid url: {},{}", s, e.getMessage());
                    }
                }
            }
            return hostPorts;
        }
    }

    public static <V> ISpringRedisCache<V> getSpringRedisCache() {
        Lock lock = ProjectRedisClient.readWriteLock.readLock();
        try {
            lock.lock();
            return ProjectRedisClientHolder.instance;
        } finally {
            lock.unlock();
        }
    }
}
